package com.example.devecohp;

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.chains.DiffRequestChain;
import com.intellij.diff.chains.SimpleDiffRequestChain;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.editor.ChainDiffVirtualFile;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.diff.util.DiffUserDataKeys;
import com.intellij.diff.util.Side;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightVirtualFile;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class represent a file that the user run the auditor on, it contains information about the file and some method
 * to access this information
 */
public class PageInfo {
    // please use the path of the file the user write as id
    private final String id;
    /**
     * the virtual file of the comparison page
     */
    private VirtualFile diffFile;
    /**
     * the Virtual File that store the output of the auditor
     */
    private VirtualFile newFile;
    /**
     * the Psi file the user run the auditor on
     */
    public PsiFile oldFile;

    /**
     * the project this file is belong to
     */
    private final Project project;
    /**
     * the id of the rule the user selected
     */
    private String currRuleId;
    /**
     * whether to show errors
     */
    public boolean showErrors =true;
    /**
     * whether to show warnings
     */
    public boolean showWarnings = true;
    /**
     * whether to show infos
     */
    public boolean showInfo = true;
    /**
     * a map to map each of the rule to a highlighter that highlight the line of text it occur, the highlighter's offset
     * will change as the user edit the file
     */
    private final Map<RuleInfo,RangeHighlighterEx> highlighterExMap;

    public PageInfo(Project project, VirtualFile newFile, PsiFile oldFile, List<RuleInfo> auditEntries, Boolean openFile){
        this.oldFile=oldFile;
        this.id = oldFile.getVirtualFile().getPath();
        this.project=project;
        this.highlighterExMap =new HashMap<>();
        updateInfo(auditEntries,newFile,openFile);

        if(openFile) {
            updateDiffPage(createFirstDiffRequest());
        }



    }


    /**
     * get the information of all rules stored in this
     * @return the information of all rules stored in this
     */
    private List<RuleInfo> getAuditEntry(){
        List<RuleInfo> result = new ArrayList<>();
        result.addAll(this.highlighterExMap.keySet());
        return result;
    }

    /**
     * create a diff request for first opening the comparison page
     * @return the result diff request
     */
    private DiffRequest createFirstDiffRequest(){
        DiffRequest request = getDiffrequestTemplet();
        request.putUserData(DiffUserDataKeys.ALIGNED_TWO_SIDED_DIFF, true);
        request.putUserData(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, false);
        return request;
    }

    /**
     * get the name of the comparison file
     * @return the name of the comparison file
     */
    private String comparisonName(){
        return oldFile.getName()+" File Comparison";
    }

    /**
     * get the DiffRequest for comparing the user's file and the output of the auditor
     * @return the DiffRequest for comparing the user's file and the output of the auditor
     */
    private DiffRequest getDiffrequestTemplet(){
        DiffContentFactory contentFactory = DiffContentFactory.getInstance();
        VirtualFile oldVirtualFile = oldFile.getVirtualFile();
        DiffContent oldcontent = contentFactory.create(project, oldVirtualFile);
        DiffContent newcontent = contentFactory.create(project,newFile);
        return new SimpleDiffRequest(comparisonName(), oldcontent, newcontent, "Base Version", "Modified Version");
    }

    /**
     * update the comparison page for this file
     * @param request the DiffRequest for the new comparison page
     */
    public void updateDiffPage(DiffRequest request){
    Application app = ApplicationManager.getApplication();
        app.invokeLater(() -> {
            DumbService.getInstance(project).runWhenSmart(() -> {
                VirtualFile oldDiffFile = this.diffFile;
                DiffRequestChain requestChain = new SimpleDiffRequestChain(request);
                this.diffFile = new ChainDiffVirtualFile(requestChain, comparisonName());
                try {
                    FileEditorManager.getInstance(project).openFile(this.diffFile, true);
                }catch (Throwable e){
                }
                if (oldDiffFile != null) {
                    FileEditorManager.getInstance(project).closeFile(oldDiffFile);
                }
            });});

    }

    public String getId(){
        return this.id;
    }
    public VirtualFile getDiffFile(){
        return diffFile;
    }

    /**
     * get the name for the virtual file that use to store the output of the auditor
     * @param oldName the file name of the file the user run the auditor on
     * @return the name for the virtual file that use to store the output of the auditor
     */
    public static String getNewPageName(String oldName){
        String newFilePath = oldName;
        if(oldName.endsWith(".ts")) {
            newFilePath = oldName.replaceFirst("\\.\\w+$", "") + ".audit.ts";
        }
        else if(oldName.endsWith(".ets")){
            newFilePath = oldName.replaceFirst("\\.\\w+$", "") + ".audit.ets";
        }else if(oldName.endsWith(".c")){
            newFilePath = oldName.replaceFirst("\\.\\w+$", "") + ".audit.c";
        }else if(oldName.endsWith(".cpp")){
            newFilePath = oldName.replaceFirst("\\.\\w+$", "") + ".audit.cpp";
        }else if(oldName.endsWith(".js")){
            newFilePath = oldName.replaceFirst("\\.\\w+$", "") + ".audit.js";
        }
        return newFilePath;
    }

    public int getInfos() {
        return (int) this.highlighterExMap.keySet().stream().filter(ruleInfo -> ruleInfo.ruleType == RuleInfo.RuleTypes.Info).count();
    }

    public int getDefects() {
        return this.highlighterExMap.keySet().size();
    }

    /**
     * get all rule message stored in this
     * @return a list of all rule message stored in this
     */
    public List<String> getAuditEntries() {
        List<String> result = new ArrayList<>();
        for(RuleInfo r:this.getAuditEntry()){
            result.add(r.ruleMessage);
        }
        return result;
    }

    public int getErrors() {
        return (int) this.highlighterExMap.keySet().stream().filter(ruleInfo -> ruleInfo.ruleType == RuleInfo.RuleTypes.Error).count();
    }

    public int getWarnings() {
        return (int) this.highlighterExMap.keySet().stream().filter(ruleInfo -> ruleInfo.ruleType == RuleInfo.RuleTypes.Warning).count();
    }

    /**
     * @return the file name of the file the user run the auditor on
     */
    public String getFileName(){
        return oldFile.getName();
    }

    /**
     * get a DiffRequest to scroll to some line number
     * @param lineNum the line number to scroll to
     * @return a DiffRequest to scroll to some line number
     */
    public DiffRequest getScrollRequest(int lineNum){
        DiffContentFactory contentFactory = DiffContentFactory.getInstance();
        VirtualFile oldVirtualFile = oldFile.getVirtualFile();
        DiffRequest request = new SimpleDiffRequest(comparisonName(), contentFactory.create(project, oldVirtualFile), contentFactory.create(project, newFile), "Base Version", "Modified Version");

        request.putUserData(DiffUserDataKeys.ALIGNED_TWO_SIDED_DIFF, true);
        request.putUserData(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, false);

        request.putUserData(DiffUserDataKeys.SCROLL_TO_LINE, new Pair<>(Side.LEFT, (lineNum - 1)));
        return request;
    }

    public String getCurrRuleId() {
        return currRuleId;
    }

    public void setCurrRuleId(String currRuleId) {
        this.currRuleId = currRuleId;
    }

    /**
     * get all rule message stored in this that satisfies the inputs
     * @param error whether to include error message
     * @param warning whether to include warning message
     * @param info whether to include info message
     * @return a list of all rule message stored in this that satisfies the inputs
     */
    public List<String> getAuditEntries(boolean error,boolean warning,boolean info) {
        List<String> result = new ArrayList<>();
        for(RuleInfo s:this.getAuditEntry()){
            if(checkRule(s,error,warning,info)){
                result.add(s.ruleMessage);
            }
        }
        return result;
    }

    /**
     * check if a rule satisfies the input
     * @param s the rule info for the rule
     * @param error whether to include error message
     * @param warning whether to include warning message
     * @param info whether to include info message
     * @return whether s satisfies the input
     */
    private boolean checkRule(RuleInfo s, boolean error, boolean warning, boolean info){
        if(s.ruleType == RuleInfo.RuleTypes.Info && info){
            return true;
        } else if (s.ruleType == RuleInfo.RuleTypes.Error && error) {
            return true;
        }else if (s.ruleType == RuleInfo.RuleTypes.Warning && warning){
            return true;
        }
        return false;
    }

    /**
     * This class store the information about a rule violation reported by the auditor
     */
    public static final class RuleInfo {
        enum RuleTypes {
            Error,
            Warning,
            Info

        }
        RuleTypes ruleType;
        String ruleMessage;

        /**
         * @param ruleString the message of the rule
         * @param type the type of the rule out put by the auditor
         */
        public RuleInfo(String ruleString, int type){
            this.ruleMessage =ruleString;
            if(type==0){
                this.ruleType = RuleTypes.Info;
            } else if (type==1) {
                this.ruleType = RuleTypes.Warning;
            }else {
                this.ruleType = RuleTypes.Error;
            }
        }

        /**
         * @return the line number in the rule message
         */
        public int getLineFromString(){
            Pattern squareBracketsPattern = Pattern.compile("\\[(\\d+)");
            Matcher squareBracketsMatcher1 = squareBracketsPattern.matcher(this.ruleMessage);
            if(squareBracketsMatcher1.find()) {
                int lineNumber = Integer.parseInt(squareBracketsMatcher1.group(1)) - 1;
                if (lineNumber < 0) {
                    return 0;
                }
                return lineNumber;
            }
            return -1;
        }
    }

    /**
     * save the setting of showErrors, showWarnings and showInfo
     * @param showErrors weather or not to show errors
     * @param showWarnings weather or not to show warnings
     * @param showInfo weather or not to show info
     */
    public void save(boolean showErrors, boolean showWarnings, boolean showInfo){
        this.showInfo =showInfo;
        this.showWarnings =showWarnings;
        this.showErrors =showErrors;
    }

    /**
     * update the information stored in this
     * @param newRuleInfo the new rule infos for this
     * @param newFile a VirtualFile that represent the new output of the auditor
     * @param openNewPage whether to open a new comparison page
     */
    public void updateInfo(List<RuleInfo> newRuleInfo, VirtualFile newFile, boolean openNewPage){
        String content = LoadTextUtil.loadText(newFile).toString();
        this.newFile= new LightVirtualFile(getNewPageName(this.oldFile.getVirtualFile().getName()),content);
        if(this.diffFile!=null) {
            FileEditorManager.getInstance(project).closeFile(this.diffFile);
        }
        highlighterExMap.clear();
        updateMap(newRuleInfo);
        if (openNewPage) {
            updateDiffPage(createFirstDiffRequest());
            DaemonCodeAnalyzer.getInstance(project).restart(oldFile); // DaemonCodeAnalyzer: Manages the background highlighting and auto-import for files displayed in editors.
        }

    }

    /**
     * update the rule info map
     * @param newRuleInfo the list of new rule info to store in this
     */
    public void updateMap(List<RuleInfo> newRuleInfo){
        Document document = PsiDocumentManager.getInstance(project).getDocument(this.oldFile);
        if(document==null){
            System.out.println(this.oldFile.getVirtualFile().getPath());
            return;
        }
        MarkupModelEx markup=(MarkupModelEx) DocumentMarkupModel.forDocument(PsiDocumentManager.getInstance(project).getDocument(this.oldFile), project, true);
        for(RuleInfo rule : newRuleInfo){
            int i = rule.getLineFromString();
            RangeHighlighterEx highlighterEx = markup.addPersistentLineHighlighter(i, HighlighterLayer.WEAK_WARNING, null);
            highlighterExMap.put(rule, highlighterEx);
        }
    }

    /**
     * update the rule info map by check whether we have new information for the null entry
     */
    public void updateMap(){
        MarkupModelEx markup=(MarkupModelEx) DocumentMarkupModel.forDocument(PsiDocumentManager.getInstance(project).getDocument(this.oldFile), project, true);
        for(RuleInfo rule : highlighterExMap.keySet()){
            if(highlighterExMap.get(rule)==null) {
                int i = rule.getLineFromString();
                RangeHighlighterEx highlighterEx = markup.addPersistentLineHighlighter(i, HighlighterLayer.WEAK_WARNING, null);
                highlighterExMap.put(rule, highlighterEx);
            }
        }
    }

    /**
     * update the LineMarkers on this page by force it to be Analyzed again
     */
    public void updateLineMarker(){
        DaemonCodeAnalyzer.getInstance(project).restart(oldFile);
    }

    /**
     * get the range of rule in current document and the rule message by the user input
     * @param error whether to include errors
     * @param warning whether to include warnings
     * @param info whether to include info
     * @return the range of rule in current document and the rule message by the user input
     */
    public Map<RangeHighlighterEx,String> getRuleElement(boolean error, boolean warning, boolean info){
        Map<RangeHighlighterEx,String> result = new HashMap<>();
        if(highlighterExMap.containsValue(null)){
            Application application = ApplicationManager.getApplication();
            application.invokeLater(() -> {
                DumbService.getInstance(project).runWhenSmart(() -> {
                    getHighlighterExMap();
                    updateLineMarker();
                });
            });
            return null;
        }
        for(RuleInfo r:this.highlighterExMap.keySet()){
            if(checkRule(r,error,warning,info)){
                result.put(this.highlighterExMap.get(r), r.ruleMessage);
            }

        }
        return result;
    }

    /**
     * get the current line number of the rule that have given message
     * @param ruleString the rule message
     * @return the line number
     */
    public int getNewLineNumber(String ruleString){
        for(RuleInfo r:this.getHighlighterExMap().keySet()){
            if(r.ruleMessage.trim().equals(ruleString.trim())){
                RangeHighlighterEx rangeHighlighterEx = this.highlighterExMap.get(r);
                if(rangeHighlighterEx!=null) {
                    return StringUtil.offsetToLineNumber(rangeHighlighterEx.getDocument().getText(), rangeHighlighterEx.getAffectedAreaStartOffset());
                }
                break;
            }
        }
        return -2;
    }

    /**
     * get the current highlighterExMap, if there are missing information, try to fill it
     * @return the current highlighterExMap after fill the missing information
     */
    private Map<RuleInfo,RangeHighlighterEx> getHighlighterExMap(){
        if(highlighterExMap.containsValue(null)){
            try {
                updateMap();
            }catch (RuntimeExceptionWithAttachments e){
                e.printStackTrace();
            }

        }
        return highlighterExMap;
    }

}
